﻿using System.Collections.Generic;

namespace Raven
{
    public class RavenGame
    {
        private EntityManager _entityManager;
        private MessageDispatcher _dispatcher;
        private PrecisionTimer _timer;
        private RavenMap _map;
        private GraveMarkers _graveMarkers;
        private PathManager<RavenPathPlanner> _pathManager;
        private List<RavenBot> _bots;
        private Random _random;

        //this list contains any active projectiles (slugs, rockets,
        //shotgun pellets, etc)
        LinkedList<RavenProjectile> _projectiles;
        private EntityPool _pool;
        private ProjectileBolt.ProjectileBoltPool _boltPool;

        public RavenGame(IQueryTime queryTime)
        {
            _pool = new EntityPool();
            _boltPool = new ProjectileBolt.ProjectileBoltPool(_pool);
            _timer = new PrecisionTimer(Constants.FrameRate, queryTime);
            _random = new Random(0);
            _entityManager = new EntityManager();
            _dispatcher = new MessageDispatcher(this);
            LoadMap(Constants.MapPath);
        }

        public EntityManager EntityManager => _entityManager;

        public MessageDispatcher Dispatcher => _dispatcher;

        public int GetSizeX()
        {
            return _map.GetSizeX();
        }

        public int GetSizeY()
        {
            return _map.GetSizeY();
        }

        public RavenMap GetMap()
        {
            return _map;
        }

        public List<RavenBot> GetAllBots()
        {
            return _bots;
        }

        public PrecisionTimer GetTimer()
        {
            return _timer;
        }

        public Random GetRandom()
        {
            return _random;
        }

        public PathManager<RavenPathPlanner> GetPathManager()
        {
            return _pathManager;
        }


        public void Update()
        {
            UnityEngine.Profiling.Profiler.BeginSample("Update");
            while (_timer.ReadyForNextFrame())
            {
                UnityEngine.Profiling.Profiler.BeginSample("InternalUpdate");
                InternalUpdate();
                UnityEngine.Profiling.Profiler.EndSample();
            }

            UnityEngine.Profiling.Profiler.EndSample();
        }

        private void InternalUpdate()
        {
            UnityEngine.Profiling.Profiler.BeginSample("_graveMarkers");
            _graveMarkers.Update();
            UnityEngine.Profiling.Profiler.EndSample();
            UnityEngine.Profiling.Profiler.BeginSample("UpdateSearches");
            _pathManager.UpdateSearches();
            UnityEngine.Profiling.Profiler.EndSample();
            //update any current projectiles
            var curW = _projectiles.First;
            UnityEngine.Profiling.Profiler.BeginSample("projectiles");
            while (curW != null)
            {
                //test for any dead projectiles and remove them if necessary
                if (!curW.Value.isDead())
                {
                    curW.Value.Update();
                }
                else
                {
                    _pool.ReturnEntitiy(curW.Value);
                    _projectiles.Remove(curW);
                }

                curW = curW.Next;
            }

            UnityEngine.Profiling.Profiler.EndSample();
            //update the bots
            bool bSpawnPossible = true;
            UnityEngine.Profiling.Profiler.BeginSample("Bot");
            foreach (var curBot in _bots)
            {
                //if this bot's status is 'respawning' attempt to resurrect it from
                //an unoccupied spawn point
                if (curBot.isSpawning() && bSpawnPossible)
                {
                    bSpawnPossible = AttemptToAddBot(curBot);
                }

                //if this bot's status is 'dead' add a grave at its current location 
                //then change its status to 'respawning'
                else if (curBot.isDead())
                {
                    //create a grave
                    _graveMarkers.AddGrave(curBot.Pos());

                    //change its status to spawning
                    curBot.SetSpawning();
                }

                //if this bot is alive update it.
                else if (curBot.isAlive())
                {
                    curBot.Update();
                }
            }

            UnityEngine.Profiling.Profiler.EndSample();
            //update the triggers
            UnityEngine.Profiling.Profiler.BeginSample("triggers");
            _map.UpdateTriggerSystem(_bots);
            UnityEngine.Profiling.Profiler.EndSample();
        }

        public bool LoadMap(string fileName)
        {
            Clear();
            _entityManager.Reset();
            _bots = new List<RavenBot>();
            _projectiles = new LinkedList<RavenProjectile>();
            _graveMarkers = new GraveMarkers(this, Constants.GraveLifetime);
            _pathManager = new PathManager<RavenPathPlanner>(Constants.MaxSearchCyclesPerUpdateStep);
            _map = new RavenMap(this);


            if (_map.LoadMap(fileName))
            {
                AddBots(Constants.NumBots);
                return true;
            }

            return false;
        }

        public void Clear()
        {
            if (_bots != null)
            {
                foreach (var bot in _bots)
                {
                    Logger.Log("deleting entity id: " + bot.ID() + " of type " + bot.GetType());
                    bot.Destroy();
                }
            }

            if (_projectiles != null)
            {
                foreach (var projectile in _projectiles)
                {
                    Logger.Log("deleting entity id: " + projectile.ID() + " of type " + projectile.GetType());
                    projectile.Destroy();
                }
            }

            if (_graveMarkers != null)
            {
                _graveMarkers.Reset();
            }
            
            _pool.Clear();

            _graveMarkers = null;
            _bots = null;
            _projectiles = null;
        }

        public void AddBots(int numBotsToAdd)
        {
            while (numBotsToAdd-- > 0)
            {
                RavenBot bot = new RavenBot(this, Vector2.zero);

                //switch the default steering behaviors on
                bot.GetSteering().WallAvoidanceOn();
                bot.GetSteering().SeparationOn();

                _bots.Add(bot);

                _entityManager.RegisterEntity(bot);
                Logger.Log("Adding bot with ID " + bot.ID());
            }
        }

        public void TagRavenBotsWithinViewRange(BaseGameEntity entity, Fix radius)
        {
            foreach (var bot in _bots)
            {
                bot.UnTag();
                Vector2 to = bot.Pos() - entity.Pos();
                Fix range = radius + bot.BRadius();

                if (bot != entity && to.sqrMagnitude < range * range)
                {
                    bot.Tag();
                }
            }
        }

        //----------------------------- AttemptToAddBot -------------------------------
        //-----------------------------------------------------------------------------
        bool AttemptToAddBot(RavenBot pBot)
        {
            //make sure there are some spawn points available
            if (_map.GetSpawnPoints().Count <= 0)
            {
                Logger.LogError("Map has no spawn points!");
                return false;
            }

            //we'll make the same number of attempts to spawn a bot this update as
            //there are spawn points
            int attempts = _map.GetSpawnPoints().Count;

            while (--attempts >= 0)
            {
                //select a random spawn point
                Vector2 pos = _map.GetRandomSpawnPoint();

                //check to see if it's occupied
                bool bAvailable = true;

                foreach (var curBot in _bots)
                {
                    //if the spawn point is unoccupied spawn a bot
                    if ((pos - curBot.Pos()).magnitude < curBot.BRadius())
                    {
                        bAvailable = false;
                    }
                }


                if (bAvailable)
                {
                    pBot.Spawn(pos);
                    //_graveMarkers.AddGrave(pBot.Pos());

                    return true;
                }
            }

            return false;
        }

        //--------------------------- AddBolt -----------------------------------------
        //-----------------------------------------------------------------------------
        public void AddBolt(RavenBot shooter, Vector2 target)
        {
            RavenProjectile rp = _boltPool.New(shooter, target);

            _projectiles.AddLast(rp);

            Logger.Log("Adding a bolt " + rp.ID() + " at pos " + rp.Pos());
        }

        public void AddRailGunSlug(RavenBot shooter, Vector2 target)
        {
            RavenProjectile rp = new ProjectileSlug(shooter, target);

            _projectiles.AddLast(rp);

            Logger.Log("Adding a rail gun slug " + rp.ID() + " at pos " + rp.Pos());
        }

        public void AddRocket(RavenBot shooter, Vector2 target)
        {
            RavenProjectile rp = new ProjectileRocket(shooter, target);

            _projectiles.AddLast(rp);

            Logger.Log("Adding a rocket " + rp.ID() + " at pos " + rp.Pos());
        }

        public void AddShotGunPellet(RavenBot shooter, Vector2 target)
        {
            RavenProjectile rp = new ProjectilePellet(shooter, target);

            _projectiles.AddLast(rp);

            Logger.Log("Adding a shotgun shell " + rp.ID() + " at pos " + rp.Pos());
        }

        //---------------------------- isLOSOkay --------------------------------------
        //
        //  returns true if the ray between A and B is unobstructed.
        //------------------------------------------------------------------------------
        public bool isLOSOkay(Vector2 A, Vector2 B)
        {
            return !WallIntersectionTests.doWallsObstructLineSegment(A, B, GetMap().GetWalls());
        }

        //------------------------- isPathObstructed ----------------------------------
        //
        //  returns true if a bot cannot move from A to B without bumping into 
        //  world geometry. It achieves this by stepping from A to B in steps of
        //  size BoundingRadius and testing for intersection with world geometry at
        //  each point.
        //-----------------------------------------------------------------------------
        public bool IsPathObstructed(Vector2 A, Vector2 B, Fix BoundingRadius)
        {
            Vector2 ToB = (B - A).normalized;

            Vector2 curPos = A;

            while ((curPos - B).sqrMagnitude > BoundingRadius * BoundingRadius)
            {
                //advance curPos one step
                curPos += ToB / Fix.Two * BoundingRadius;

                //test all walls against the new position
                if (WallIntersectionTests.doWallsIntersectCircle(_map.GetWalls(), curPos, BoundingRadius))
                {
                    return true;
                }
            }

            return false;
        }
    }
}